/*******************************************************
 * Copyright (c) 2014, ArrayFire
 * All rights reserved.
 *
 * This file is distributed under 3-clause BSD license.
 * The complete license agreement can be obtained at:
 * http://arrayfire.com/licenses/BSD-3-Clause
 ********************************************************/

#include <arrayfire.h>
#include <gtest/gtest.h>
#include <testHelpers.hpp>
#include <af/dim4.hpp>
#include <af/traits.hpp>
#include <iostream>
#include <string>
#include <vector>

using af::array;
using af::cdouble;
using af::cfloat;
using af::dim4;
using af::dtype_traits;
using std::abs;
using std::cout;
using std::endl;
using std::string;
using std::vector;

template<typename T>
class Rotate : public ::testing::Test {
   public:
    virtual void SetUp() {}
};

// create a list of types to be tested
typedef ::testing::Types<float, double, cfloat, cdouble, int, intl, char, short>
    TestTypes;

// register the type list
TYPED_TEST_CASE(Rotate, TestTypes);

#define PI 3.1415926535897931f

template<typename T>
void rotateTest(string pTestFile, const unsigned resultIdx, const float angle,
                const bool crop) {
    SUPPORTED_TYPE_CHECK(T);

    vector<dim4> numDims;
    vector<vector<T> > in;
    vector<vector<T> > tests;
    readTests<T, T, float>(pTestFile, numDims, in, tests);

    dim4 dims = numDims[0];

    af_array inArray   = 0;
    af_array outArray  = 0;
    af_array tempArray = 0;

    float theta = angle * PI / 180.0f;

    ASSERT_SUCCESS(af_create_array(&inArray, &(in[0].front()), dims.ndims(),
                                   dims.get(),
                                   (af_dtype)dtype_traits<T>::af_type));

    ASSERT_SUCCESS(
        af_rotate(&outArray, inArray, theta, crop, AF_INTERP_NEAREST));

    // Get result
    T* outData = new T[tests[resultIdx].size()];
    ASSERT_SUCCESS(af_get_data_ptr((void*)outData, outArray));

    // Compare result
    size_t nElems = tests[resultIdx].size();

    // This is a temporary solution. The reason we need this is because of
    // floating point error in the index computations on CPU/GPU, some
    // elements of GPU(CUDA/OpenCL) versions are different from the CPU version.
    // That is, the input index of CPU/GPU may differ by 1 (rounding error) on
    // x or y, hence a different value is copied.
    // We expect 99.99% values to be same between the CPU/GPU versions and
    // ASSERT_EQ (in comments below) to pass for CUDA & OpenCL backends
    size_t fail_count = 0;
    for (size_t i = 0; i < nElems; i++) {
        if (abs((tests[resultIdx][i] - (T)outData[i])) > 0.001) fail_count++;
    }
    ASSERT_EQ(true, ((fail_count / (float)nElems) < 0.005));

    // for (size_t elIter = 0; elIter < nElems; ++elIter) {
    //    ASSERT_EQ(tests[resultIdx][elIter], outData[elIter]) << "at: " <<
    //    elIter << endl;
    //}

    // Delete
    delete[] outData;

    if (inArray != 0) af_release_array(inArray);
    if (outArray != 0) af_release_array(outArray);
    if (tempArray != 0) af_release_array(tempArray);
}

#define ROTATE_INIT(desc, file, resultIdx, angle, crop)                  \
    TYPED_TEST(Rotate, desc) {                                           \
        rotateTest<TypeParam>(string(TEST_DIR "/rotate/" #file ".test"), \
                              resultIdx, angle, crop);                   \
    }

ROTATE_INIT(Square180NoCropRecenter, rotate1, 0, 180, false);
ROTATE_INIT(Square180CropRecenter, rotate1, 1, 180, true);
ROTATE_INIT(Square90NoCropRecenter, rotate1, 2, 90, false);
ROTATE_INIT(Square90CropRecenter, rotate1, 3, 90, true);
ROTATE_INIT(Square45NoCropRecenter, rotate1, 4, 45, false);
ROTATE_INIT(Square45CropRecenter, rotate1, 5, 45, true);
ROTATE_INIT(Squarem45NoCropRecenter, rotate1, 6, -45, false);
ROTATE_INIT(Squarem45CropRecenter, rotate1, 7, -45, true);
ROTATE_INIT(Square60NoCropRecenter, rotate1, 8, 60, false);
ROTATE_INIT(Square60CropRecenter, rotate1, 9, 60, true);
ROTATE_INIT(Square30NoCropRecenter, rotate1, 10, 30, false);
ROTATE_INIT(Square30CropRecenter, rotate1, 11, 30, true);
ROTATE_INIT(Square15NoCropRecenter, rotate1, 12, 15, false);
ROTATE_INIT(Square15CropRecenter, rotate1, 13, 15, true);
ROTATE_INIT(Square10NoCropRecenter, rotate1, 14, 10, false);
ROTATE_INIT(Square10CropRecenter, rotate1, 15, 10, true);
ROTATE_INIT(Square01NoCropRecenter, rotate1, 16, 1, false);
ROTATE_INIT(Square01CropRecenter, rotate1, 17, 1, true);
ROTATE_INIT(Square360NoCropRecenter, rotate1, 18, 360, false);
ROTATE_INIT(Square360CropRecenter, rotate1, 19, 360, true);
ROTATE_INIT(Squarem180NoCropRecenter, rotate1, 20, -180, false);
ROTATE_INIT(Squarem180CropRecenter, rotate1, 21, -180, false);
ROTATE_INIT(Square00NoCropRecenter, rotate1, 22, 0, false);
ROTATE_INIT(Square00CropRecenter, rotate1, 23, 0, true);

ROTATE_INIT(Rectangle180NoCropRecenter, rotate2, 0, 180, false);
ROTATE_INIT(Rectangle180CropRecenter, rotate2, 1, 180, true);
ROTATE_INIT(Rectangle90NoCropRecenter, rotate2, 2, 90, false);
ROTATE_INIT(Rectangle90CropRecenter, rotate2, 3, 90, true);
ROTATE_INIT(Rectangle45NoCropRecenter, rotate2, 4, 45, false);
ROTATE_INIT(Rectangle45CropRecenter, rotate2, 5, 45, true);
ROTATE_INIT(Rectanglem45NoCropRecenter, rotate2, 6, -45, false);
ROTATE_INIT(Rectanglem45CropRecenter, rotate2, 7, -45, true);
ROTATE_INIT(Rectangle60NoCropRecenter, rotate2, 8, 60, false);
ROTATE_INIT(Rectangle60CropRecenter, rotate2, 9, 60, true);
ROTATE_INIT(Rectangle30NoCropRecenter, rotate2, 10, 30, false);
ROTATE_INIT(Rectangle30CropRecenter, rotate2, 11, 30, true);
ROTATE_INIT(Rectangle15NoCropRecenter, rotate2, 12, 15, false);
ROTATE_INIT(Rectangle15CropRecenter, rotate2, 13, 15, true);
ROTATE_INIT(Rectangle10NoCropRecenter, rotate2, 14, 10, false);
ROTATE_INIT(Rectangle10CropRecenter, rotate2, 15, 10, true);
ROTATE_INIT(Rectangle01NoCropRecenter, rotate2, 16, 1, false);
ROTATE_INIT(Rectangle01CropRecenter, rotate2, 17, 1, true);
ROTATE_INIT(Rectangle360NoCropRecenter, rotate2, 18, 360, false);
ROTATE_INIT(Rectangle360CropRecenter, rotate2, 19, 360, true);
ROTATE_INIT(Rectanglem180NoCropRecenter, rotate2, 20, -180, false);
ROTATE_INIT(Rectanglem180CropRecenter, rotate2, 21, -180, false);
ROTATE_INIT(Rectangle00NoCropRecenter, rotate2, 22, 0, false);
ROTATE_INIT(Rectangle00CropRecenter, rotate2, 23, 0, true);

////////////////////////////////// CPP //////////////////////////////////////
//
TEST(Rotate, CPP) {
    const unsigned resultIdx = 0;
    const float angle        = 180;
    const bool crop          = false;

    vector<dim4> numDims;
    vector<vector<float> > in;
    vector<vector<float> > tests;
    readTests<float, float, float>(string(TEST_DIR "/rotate/rotate1.test"),
                                   numDims, in, tests);

    dim4 dims   = numDims[0];
    float theta = angle * PI / 180.0f;

    array input(dims, &(in[0].front()));
    array output = rotate(input, theta, crop, AF_INTERP_NEAREST);

    // Get result
    float* outData = new float[tests[resultIdx].size()];
    output.host((void*)outData);

    // Compare result
    size_t nElems = tests[resultIdx].size();

    // This is a temporary solution. The reason we need this is because of
    // floating point error in the index computations on CPU/GPU, some
    // elements of GPU(CUDA/OpenCL) versions are different from the CPU version.
    // That is, the input index of CPU/GPU may differ by 1 (rounding error) on
    // x or y, hence a different value is copied.
    // We expect 99.99% values to be same between the CPU/GPU versions and
    // ASSERT_EQ (in comments below) to pass for CUDA & OpenCL backends
    size_t fail_count = 0;
    for (size_t i = 0; i < nElems; i++) {
        if (fabs(tests[resultIdx][i] - outData[i]) > 0.0001) fail_count++;
    }
    ASSERT_EQ(true, ((fail_count / (float)nElems) < 0.01));

    // Delete
    delete[] outData;
}
